package org.chorus;

import java.io.IOException;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

import org.gicentre.utils.stat.AbstractChart;
import org.gicentre.utils.stat.XYChart;

import jwo.landserf.structure.RasterMap;

import processing.core.PApplet;
import processing.core.PConstants;
import processing.core.PGraphics;
import processing.core.PVector;

//  **********************************************************************************
/** Sketch for creating and viewing PDF (Probability Density Function) graphs of DEMs.
 *  @author Jo Wood
 *  @version 1.3.2, 18th March, 2013.
 */
//  **********************************************************************************

/* This file is part of the Chorus river bed analysis software package. Chorus is free software:
 * you can redistribute it and/or modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 * 
 * Chorus is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this
 * source code (see COPYING included with this source code). If not, see 
 * http://www.gnu.org/licenses/.
 */

public class PDFGrapher implements Modular,Comparable<Modular>
{
	// ------------------------ Object and class variables --------------------------

	private PApplet parent;						// Parent applet in which this class is embedded.
	private PGraphics graphics;					// Graphics context in which to send output.
	private ArrayList<XYChart> charts;			// Collection of charts to display.
	private ArrayList<RasterSummary>summaries;	// Collection of raster summaries, one per chart.
	private ArrayList<Float>bandwidths;			// Bandwidth for each PDF.
	private boolean doPDFrecalc;
	private float maxFreq;						// Global maximum frequency of any PDF functions.
	private int colourOffset;					// Used to select starting palette colour.
	private int order;							// Order number for sorted modules.
	private String units;						// Planimetric units to be labelled on the PDF x-axis.
	private float maxProb,minElev,maxElev;		// For scaling between graphs.
	
	private static DecimalFormat D_FORMATTER = new DecimalFormat( "##0.000000" );

	// ---------------------------------- Methods ----------------------------------

	/** Initialises the grapher.
	 *  @param parent Parent class in which the PDF grapher is embedded.
	 */
	public void setup(PApplet parent)
	{
		this.parent = parent;
		charts = new ArrayList<XYChart>();
		summaries = new ArrayList<RasterSummary>();
		bandwidths = new ArrayList<Float>();
		doPDFrecalc = true;
		maxFreq = 0;
		units = "mm";
		colourOffset = 0;
		maxProb = 0;
		minElev = Float.MAX_VALUE;
		maxElev = -Float.MAX_VALUE;
		setGraphics(parent.g);
	}

	/** Sets the the graphics context into which all output is directed. This method allows
	 *  output to be redirected to print output, offscreen buffers etc.
	 *  @param graphics New graphics context in which the modular class is embedded.
	 */
	public void setGraphics(PGraphics graphics)
	{
		this.graphics = graphics;

		for (AbstractChart chart : charts)
		{
			chart.setGraphics(graphics);
		}
	}

	/** Sets the order number for this module. Allows a list of modules to be sorted in some arbitrary order.
	 *  @param order Order number where the smaller the number, the earlier in a sorted list the module will appear.
	 */
	public void setOrder(int order)
	{
		this.order = order;
	}

	/** Reports the order number for this module. Allows a list of modules to be sorted in some arbitrary order.
	 *  @return Order number where the smaller the number, the earlier in a sorted list the module will appear.
	 */
	public int getOrder()
	{
		return order;
	}

	/** Sets the units labels to be displayed on the horizontal axis of the PDF.
	 *  @param units Units labels (e.g. 'mm' or 'cm').
	 */
	public void setUnits(String units)
	{
		this.units = units;

		for (XYChart chart : charts)
		{
			chart.setXAxisLabel("Height ("+units+")");
		}
	}

	/** Sets the maximum probability value to be shown on the PDF graph. Allow multiple PDF graphs to share the same scale.
	 *  @param maxProb Maximum probability.
	 */
	public void setMaxProb(float maxProb)
	{
		for (XYChart chart : charts)
		{
			chart.setMaxY(maxProb);
			//chart.transposeAxes(false);
		}
	}

	/** Reports the maximum probability to be shown on the PDF graph. This may not be the same as the actual maximum,
	 *  but rather the maximum value on the vertical axis.
	 *  @return Maximum probability shown by the graph.
	 */
	public float getMaxProb()
	{
		// TODO: Fix external setting of min/max values.
		return maxProb;
	}

	/** Reports the maximum elevation to be shown on the PDF graph. This may not be the same as the actual maximum,
	 *  but rather the maximum value on the horizontal axis.
	 *  @return Maximum elevation shown by the graph.
	 */
	public float getMaxElev()
	{
		// TODO: Fix external setting of min/max values.
		return maxElev;
	}

	/** Reports the minimum elevation to be shown on the PDF graph. This may not be the same as the actual minimum,
	 *  but rather the minimum value on the horizontal axis.
	 *  @return Minimum elevation shown by the graph.
	 */
	public float getMinElev()
	{
		// TODO: Fix external setting of min/max values.
		return minElev;
	}
	
	/** Reports the summary raster statistics for those displayed as PDFs by this module.
	 *  @return List of summary statistics, one collection for each DEM in this module.
	 */
	public List<RasterSummary> getSummaries()
	{
		return summaries;
	}

	/** Sets the maximum elevation value to be shown on the PDF graph. Allow multiple PDF graphs to share the same scale.
	 *  @param maxElev Maximum elevation value.
	 */
	public void setMaxElev(float maxElev)
	{
		// TODO: Fix external setting of min/max values.
		for (XYChart chart : charts)
		{
			chart.setMaxX(maxElev);
			//chart.transposeAxes(false);
		}
	}

	/** Sets the minimum elevation value to be shown on the PDF graph. Allow multiple PDF graphs to share the same scale.
	 *  @param minElev Minimum elevation value.
	 */
	public void setMinElev(float minElev)
	{
		// TODO: Fix external setting of min/max values.

		for (XYChart chart : charts)
		{
			chart.setMinX(minElev);
			//chart.transposeAxes(false);
		}
	}

	/** Writes out the PDF values to a text file.
	 *  @param fileName Name of output file.
	 *  @return True if output is written successfully.
	 */
	public boolean writeOutput(String fileName)
	{
		try
		{
			PrintWriter output = new PrintWriter(fileName);
			ArrayList<float[]>densities = new ArrayList<float[]>();
			float[] heights = null;
			StringBuffer titleText = new StringBuffer();

			for (int i=0; i<charts.size(); i++)
			{
				if (i==0)
				{
					titleText.append("density("+summaries.get(i).getTitle()+")");
				}
				else
				{
					titleText.append(", density("+summaries.get(i).getTitle()+")");
				}
				
				// Get PDF data.
				XYChart chart = charts.get(i);

				if (heights == null)
				{
					heights = chart.getXData();
				}
				else if (heights.length != chart.getXData().length)
				{
					System.err.println("Warning: PDF values have different height ranges");
				}
				densities.add(chart.getYData());
			}
			if (heights == null)
			{
				System.err.println("Problem extracting PDF values.");
				output.close();
				return false;
			}
			
			output.println("# Height, "+titleText.toString());
			
			for (int i=0; i<heights.length; i++)
			{
				output.print(heights[i]);
				for (float[] density : densities)
				{
					output.print(","+D_FORMATTER.format(density[i]));
				}
				output.println();
			}

			output.flush();
			output.close();
			System.out.println("PDF values written to "+fileName);
		}
		catch (IOException e)
		{
			System.err.println("Problem writing PDF output to "+fileName+"': "+e);
			return false;
		}
		
		return true;
	}

	/** Draws the PDF graph and summary statistics.
	 *  @param xOrigin Left-hand pixel coordinate of the area in which to draw graph.
	 *  @param yOrigin Top pixel coordinate of the area in which to draw graph.
	 *  @param width Width in pixels of the area in which to draw graph.
	 *  @param height Height in pixels of the area in which to draw graph.
	 */
	public void draw(float xOrigin, float yOrigin, float width, float height)
	{
		// Check to see that we have at least one DEM to display		
		if (summaries.size() == 0)
		{
			graphics.fill(graphics.color(80));
			graphics.textFont(DisplayUtils.mediumFont);
			graphics.textAlign(PConstants.CENTER, PConstants.CENTER);
			graphics.text("No DEMs available for PDF",xOrigin+width/2,yOrigin+height/2);
			return;
		}

		// Update PDF on drawing thread if requested.
		if (doPDFrecalc == true)
		{
			maxFreq = 0;
			for (int i=0; i<summaries.size(); i++)
			{
				RasterSummary summary = summaries.get(i);
				XYChart chart = charts.get(i);				
				summary.buildPDF(bandwidths.get(i).floatValue());
				updateChart(chart,summary);
			}	

			// Update all charts to reflect a possibly new global maximum frequency
			for (XYChart pdfChart: charts)
			{
				pdfChart.setMinY(0);
				pdfChart.setMaxY(maxFreq);
				pdfChart.showXAxis(true);
				pdfChart.showYAxis(true);
			}

			doPDFrecalc = false;
		}


		// Find width of all text labels and scale down if too wide for display area.
		graphics.textFont(DisplayUtils.smallFont);
		float maxBlockWidth  = 0;
		float maxBlockHeight = 0;
		PVector topLeftPos = new PVector(xOrigin,yOrigin);

		for (int i=0; i<summaries.size(); i++)
		{
			RasterSummary demSummary = summaries.get(i);
			PVector blockSize = displaySummaryText(demSummary,topLeftPos,1,false);
			topLeftPos.x += blockSize.x+10;
			maxBlockHeight = Math.max(maxBlockHeight,blockSize.y);
		}

		maxBlockWidth = topLeftPos.x-xOrigin;
		float textScale = Math.min(1,width/maxBlockWidth);

		maxBlockHeight *= textScale;

		// Draw PDFs with mean and standard deviations marked.
		topLeftPos= new PVector(xOrigin,yOrigin);
		graphics.strokeWeight(0.3f);		// Weight of mean and standard deviation markers.

		for (int i=0; i<summaries.size(); i++)
		{
			XYChart chart = charts.get(i);
			RasterSummary demSummary = summaries.get(i);

			int chartColour;
			if (summaries.size() == 1)
			{
				chartColour = parent.color(0);
			}
			else
			{
				chartColour = DisplayUtils.getCategoryPalette().findColour(colourOffset+i);
			}

			// Show summary stats as text.
			graphics.fill(chartColour);
			graphics.textFont(DisplayUtils.smallFont);

			PVector blockSize = displaySummaryText(demSummary,topLeftPos,textScale,true);
			topLeftPos.x += blockSize.x+10*textScale;

			// Draw the PDF charts.
			chart.setLineColour(parent.color(chartColour,200));
			graphics.textFont(DisplayUtils.smallFont);
			chart.draw(xOrigin, yOrigin+maxBlockHeight, width-0,height-maxBlockHeight);

			float upper = demSummary.getMean()+demSummary.getStdev();
			float lower = demSummary.getMean()-demSummary.getStdev();

			PVector baselinePos   = chart.getDataToScreen(new PVector(chart.getXData()[0],0));
			PVector meanPos       = chart.getDataToScreen(new PVector(demSummary.getMean(),demSummary.getProbability(demSummary.getMean())));
			PVector plusStdevPos  = chart.getDataToScreen(new PVector(upper,demSummary.getProbability(upper)));
			PVector minusStdevPos = chart.getDataToScreen(new PVector(lower,demSummary.getProbability(lower)));

			graphics.stroke(chartColour,150);
			graphics.line(meanPos.x, baselinePos.y, meanPos.x, meanPos.y);
			graphics.stroke(chartColour,80);
			graphics.line(plusStdevPos.x, baselinePos.y, plusStdevPos.x, plusStdevPos.y);
			graphics.line(minusStdevPos.x, baselinePos.y, minusStdevPos.x, minusStdevPos.y);
		}
	}


	/** Sets the colour palette start colour for display.
	 *  @param offset Position in default colour palette in which the first PDF is displayed.
	 */
	public void setColourOffset(int offset)
	{
		this.colourOffset = offset;
	}

	/** Would respond to mouse press events but does nothing in this case.
	 */
	public void mousePressed()
	{
		// No mouse events handled.
	}

	/** Would respond to mouse release events but does nothing in this case.
	 */
	public void mouseReleased()
	{
		// No mouse events handled.
	}

	/** Would respond to mouse drag events but does nothing in this case.
	 */
	public void mouseDragged()
	{
		// No mouse events handled.
	}

	/** Responds to key presses by updating the currently graph display.
	 * /
	public void keyPressed()
	{
		if (key == CODED)
		{
			if (keyCode == LEFT)
			{
				for (int i=0;i<bandwidths.size();i++)
				{
					bandwidths.set(i, new Float(bandwidths.get(i).floatValue()*0.9));
				}
				doPDFrecalc = true;
				loop();
			}
			else if (keyCode == RIGHT)
			{
				for (int i=0;i<bandwidths.size();i++)
				{
					bandwidths.set(i, new Float(bandwidths.get(i).floatValue()*1.1));
				}
				doPDFrecalc = true;
				loop();
			}
		}
	}
	 */

	/** Adds the given DEM to those whose PDFs are displayed.
	 *  @param dem DEM to add.
	 */
	public void addDEM(RasterMap dem) 
	{
		if (dem != null)
		{
			summaries.add(new RasterSummary(dem));
			bandwidths.add(new Float(summaries.get(summaries.size()-1).getStdev()/5f));
			addChart();
		}
	}

	/** Allows this module to be compared with another to determine sort order.
	 *  @param otherModule Other module to compare with this one.
	 */
	public int compareTo(Modular otherModule)
	{
		int comparison = order-otherModule.getOrder();
		if (comparison == 0)
		{
			return hashCode()-otherModule.hashCode();
		}
		return comparison;
	}

	// ------------------------------ Private methods ------------------------------

	/** Adds a new chart to those being displayed. Ensures that all charts have the same
	 *  x and y scales so magnitudes and spreads can be compared.
	 */
	private void addChart()
	{		
		XYChart newChart = new XYChart(parent);		
		newChart.setMinY(0);
		newChart.setMaxY(0.8f);
		newChart.setPointSize(0);
		newChart.setLineWidth(1.5f);
		if (charts.size() > 0)
		{
			newChart.setDecorations(false);
		}
		charts.add(newChart);

		// Set graph x-range to the global minimum and maximum elevations.
		for (RasterSummary summary : summaries)
		{
			minElev = Math.min(minElev, summary.getMinVal());
			maxElev = Math.max(maxElev, summary.getMaxVal());
		}

		for (XYChart chart: charts)
		{
			chart.setMinX(minElev);
			chart.setMaxX(maxElev);
		}
	}

	/** Updates the given chart with the latest PDF values from the given DEM summary.
	 *  @param chart Chart to update
	 *  @param demSummary Raster summary data used to update the chart.
	 */
	private void updateChart(XYChart chart, RasterSummary demSummary)
	{
		float range = demSummary.getMaxVal()-demSummary.getMinVal();
		float border = range*0.05f;

		ArrayList<Float>xs = new ArrayList<Float>();
		ArrayList<Float>ys = new ArrayList<Float>();

		for (float x=demSummary.getMinVal()-border; x<=demSummary.getMaxVal()+border; x+= range/150f)
		{
			float y = demSummary.getProbability(x);
			maxFreq = Math.max(maxFreq, y);
			ys.add(new Float(y));
			xs.add(new Float(x));
		}
		float[] xData = new float[xs.size()];
		float[] yData = new float[ys.size()];

		for (int i=0; i<xs.size(); i++)
		{
			xData[i] = xs.get(i).floatValue();
			yData[i] = ys.get(i).floatValue();
		}

		chart.setData(xData,yData);
		chart.setYFormat("0.00");
		chart.setXAxisLabel("Height ("+units+")");
		chart.setYAxisLabel(" ");
		chart.showXAxis(true);
		chart.showYAxis(true);
	}

	/** Displays text summarising statistical properties of the given surface.
	 *  @param demSummary Summary data to be represented.
	 *  @param tlCorner Pixel coordinates of the top-left corner of the text block.
	 *  @param scaleFactor Scaling factor that ensures all text fits within the display area.
	 *  @param displayText Determines if text is displayed or not. Setting this to false can be used to measure the space required to display text. 
	 *  @return The width and height of this text block in pixels.
	 */
	private PVector displaySummaryText(RasterSummary demSummary, PVector tlCorner, float scaleFactor, boolean displayText)
	{
		float textHeight = graphics.textAscent()+graphics.textDescent()+2;

		String[] statTypes = new String[] {" Mean: ", " Stdev: "," Skew: ", " Kurtosis: "};

		String[] stats = new String[] {formatFloat(demSummary.getMean()),
				formatFloat(demSummary.getStdev()),
				formatFloat(demSummary.getSkew()),
				formatFloat(demSummary.getKurt())};
		float maxLabelWidth = 0;
		float maxWholeNumWidth = 0;
		float[] wholeNumWidths = new float[stats.length];
		for (int i=0; i<stats.length; i++)
		{
			String stat = stats[i];
			wholeNumWidths[i] = parent.textWidth(stat.substring(0,stat.indexOf(".")));
			maxWholeNumWidth = Math.max(maxWholeNumWidth,wholeNumWidths[i]);
			maxLabelWidth = Math.max(maxLabelWidth, parent.textWidth(statTypes[i]));
		}

		float hBaseline = tlCorner.x+maxLabelWidth;
		float currentPos = tlCorner.y;
		float blockWidth=0;

		if (displayText)
		{
			graphics.textAlign(PConstants.LEFT,PConstants.TOP);
			graphics.pushMatrix();
			graphics.translate(tlCorner.x, tlCorner.y);
			graphics.scale(scaleFactor,scaleFactor);
			graphics.translate(-tlCorner.x, -tlCorner.y);
			graphics.text(" "+demSummary.getTitle(), tlCorner.x,currentPos);
		}

		blockWidth = Math.max(blockWidth, graphics.textWidth(demSummary.getTitle()));
		currentPos += textHeight;

		for (int i=0; i<stats.length; i++)
		{
			if (displayText)
			{
				graphics.textAlign(PConstants.RIGHT,PConstants.TOP);
				graphics.text(statTypes[i],hBaseline,currentPos);
				graphics.textAlign(PConstants.LEFT,PConstants.TOP);
				graphics.text(stats[i], hBaseline+maxWholeNumWidth-wholeNumWidths[i],currentPos);
			}
			blockWidth = Math.max(blockWidth, graphics.textWidth(stats[i])+maxLabelWidth);
			currentPos += textHeight;
		}	
		if (displayText)
		{
			graphics.popMatrix();
		}

		return new PVector(blockWidth*scaleFactor,currentPos-tlCorner.y);
	}


	/** Provides a formatted text representation of a floating point number.
	 *  @param number Number to format.
	 *  @return Formatted text representation of the number.
	 */
	private String formatFloat(float number)
	{
		// This is necessary to avoid -0.000 
		if (Math.abs(number) < 0.0005)
		{
			return "0.000";
		}

		// Format to 3 decimal places
		return PApplet.nfc(number,3);
	}

}